#! /usr/bin/python3
# depends python3-magic, git-buildpackage, python3-debian

import argparse
import os
import glob
import shutil
import configparser
import time
from debian.changelog import Changelog, get_maintainer
import subprocess as p
import magic
from gbp.deb.dscfile import DscFile


def xz_to_gz(fn: str) -> str:
    new_fn = f"{os.path.splitext(fn)[0]}.gz"
    p.check_call(f"xzcat {fn} | gzip -c - > {new_fn}", shell=True)
    os.remove(fn)
    return new_fn


def gz_to_xz(fn: str) -> str:
    new_fn = f"{os.path.splitext(fn)[0]}.xz"
    p.check_call(f"zcat {fn} | xz -c - > {new_fn}", shell=True)
    os.remove(fn)
    return new_fn


class Package:
    dsc_file = source_path = ""

    def __init__(self, dsc_file: str = "", source_path: str = "") -> None:
        assert (
            dsc_file != "" or source_path != ""
        ), "you must specify a dsc file or exact source path"
        availed_path_str = dsc_file if dsc_file else source_path
        self.file_path = os.path.dirname(os.path.realpath(availed_path_str))
        if dsc_file and not source_path:
            self.dsc_file = dsc_file
            self.exact_source()
        if source_path:
            self.source_path = source_path
        self._parse_source_info()
        self._parse_dsc_file()

    def _parse_dsc_file(self):
        if not self.dsc_file:
            self.dsc_file = glob.glob(
                f"{self.file_path}/*_%s.dsc"
                % self.changelog_info.full_version.split(":")[-1]
            )[0]
        self.gbp_dsc = DscFile(self.dsc_file)
        self.additional_tarballs = self.gbp_dsc.additional_tarballs

    def _parse_source_info(self):
        with open(os.path.join(self.source_path, "debian/changelog"), "rb") as f:
            self.changelog_info = Changelog(file=f)
            self.old_version = self.changelog_info.full_version
        format_file = os.path.join(self.source_path, "debian/source/format")
        if os.path.exists(format_file):
            format = open(format_file).read().strip()
            self.source_format = "native" if format != "3.0 (quilt)" else "quilt"
        else:
            self.source_format = "native"

    @property
    def is_quilt(self) -> bool:
        return self.source_format == "quilt"

    def exact_source(self):
        if getattr(self, "source_path", ""):
            return
        tmp_source_path = f"rebuild_tmp_source_{time.time()}"
        p.check_call(
            f"dpkg-source -x {self.dsc_file} {tmp_source_path}",
            shell=True,
            cwd=self.file_path,
        )
        self.source_path = os.path.join(self.file_path, tmp_source_path)
        self._parse_source_info()

    def change_format(self):
        debian_source = os.path.join(self.source_path, "debian/source")
        if not os.path.exists(debian_source):
            os.makedirs(debian_source)
        # switch to quilt
        format_file = os.path.join(debian_source, "format")
        with open(format_file, "w") as f:
            f.write("3.0 (quilt)\n")

    def add_gbp_conf(self):
        if self.additional_tarballs:
            self.gbp_conf = os.path.join(self.source_path, "debian/gbp.conf")
            cp = configparser.ConfigParser()
            cp.read(self.gbp_conf, encoding="utf-8")
            if not cp.has_section("buildpackage"):
                cp.add_section("buildpackage")
            values = list(self.additional_tarballs.keys())
            if len(values) == 1:
                values = values[0]
            else:
                _tmp_values = "','".join(values)
                values = f"[{_tmp_values}]"
            if not cp.has_option("buildpackage", "component"):
                cp.set("buildpackage", "component", values)
            with open(self.gbp_conf, "w+") as f:
                cp.write(f)

    def fix_additional_tarballs_comp_format(self):
        # check orig tarballs compres type
        tgz_comp_type = magic.from_file(self.gbp_dsc.tgz, mime=True)
        is_xz = "x-xz" in tgz_comp_type
        is_gz = "gzip" in tgz_comp_type
        # is_bz =
        for fn in self.additional_tarballs.values():
            if magic.from_file(fn, mime=True) != tgz_comp_type:
                # do recompres
                new_fn = fn
                if is_xz:
                    new_fn = gz_to_xz(fn)
                elif is_gz:
                    new_fn = xz_to_gz(fn)
                else:
                    raise Exception(f"no support compress type for {tgz_comp_type}")
                # shutil.move(new_fn, self.source_path)

    def build_changelog(
        self,
        new_revisions: str = "",
        save_old_changes: bool = True,
        suite: str = "",
        changes: str = "",
    ):
        user, email = get_maintainer()
        revisions = (
            new_revisions if new_revisions else self.changelog_info.debian_revision
        )
        v = self.changelog_info.get_version()
        setattr(v, "debian_revision", revisions)

        suite = self.changelog_info.distributions if not suite else suite
        self.changelog_info.new_block(
            package=self.changelog_info.package,
            version=v,
            distributions=suite,
            urgency=self.changelog_info.urgency,
            author=f"{user} <{email}>",
            date=time.strftime("%a, %d %b %Y %H:%M:%S %z"),
        )
        self.changelog_info.add_change("")
        self.changelog_info.add_change(f"  * {changes}")
        self.changelog_info.add_change("")

        if not save_old_changes:
            os.remove(os.path.join(self.source_path, "debian/changelog"))
            self.changelog_info._blocks[:1]
        with open(os.path.join(self.source_path, "debian/changelog"), "w") as f:
            self.changelog_info.write_to_open_file(f)

    def rebuild_source(self, args):
        new_revisions = "" if not args.new_revisions else args.new_revisions
        if not self.is_quilt:
            new_revisions = new_revisions if new_revisions else "1"
            self.change_format()
            p.call(["rm", "-rf" "debian/patches/series"], cwd=self.source_path)
            pc = os.path.join(self.source_path, ".pc")
            if os.path.exists(pc):
                shutil.rmtree(pc)
            self.build_changelog(
                suite=args.suite,
                new_revisions=new_revisions,
                save_old_changes=args.save_old_changes,
                changes="修改为quilt格式",
            )
            p.check_call(
                ["debmake", "-y", "-t", "-u", self.changelog_info.upstream_version],
                cwd=self.source_path,
            )
        elif new_revisions:
            self.build_changelog(
                suite=args.suite,
                new_revisions=new_revisions,
                save_old_changes=args.save_old_changes,
                changes="rebuild source for openKylin",
            )

        self.add_gbp_conf()
        self.fix_additional_tarballs_comp_format()

        p.check_call("dpkg-source -b .", shell=True, cwd=self.source_path)
        self.orig_dsc_file = self.dsc_file
        self.dsc_file = self.dsc_file.replace(self.old_version, self.changelog_info.full_version)
        return os.path.abspath(self.dsc_file)


def rebuild_source(args: argparse.Namespace):
    pkg = Package(dsc_file=args.dsc_file, source_path=args.source_path)
    try:
        pkg.rebuild_source(args=args)
    except Exception as e:
        print(e)
    finally:
        shutil.rmtree(pkg.source_path)


def import_to_git(args):
    branch = args.packaging_branch
    dsc = args.dsc_file
    assert os.path.exists(dsc), f"no such {dsc} file"
    DSC = DscFile(dsc)
    p.check_call(
        f"gbp import-dsc --pristine-tar --debian-branch={branch} --create-missing-branches --upstream-branch=upstream {dsc} {DSC.pkg}",
        shell=True,
    )
    os.chdir(DSC.pkg)
    p.check_call(f"git checkout {branch}", shell=True)
    p.check_call(f"git checkout -b packaging/{branch}", shell=True)
    p.check_call("gbp pq import", shell=True)
    p.check_call("gbp pq export", shell=True)
    p.check_call("git add debian", shell=True)
    p.check_call('git commit -m "format patches" || true', shell=True)
    p.check_call(f"git checkout {branch}", shell=True)
    p.check_call(
        f'git merge patch-queue/packaging/{branch} -m "apply patches"', shell=True
    )
    p.check_call(f"git branch -D patch-queue/packaging/{branch}", shell=True)
    patches = os.path.join(DSC.pkg, "debian/patches")
    if os.path.exists(patches):
        shutil.rmtree(patches)
    p.check_call('echo "3.0 (native)" > debian/source/format', shell=True)
    p.check_call("git add debian", shell=True)
    p.check_call('git commit -m "changed debian/source/format to native"', shell=True)


def confirm(content: str, result: list = ["y", "n", "yes", "no"]) -> str:
    print(content, end="")
    _result = input()
    if _result.lower() not in result:
        confirm(content=content)
    return _result


def import_branch(args):
    packaging_branch = args.packaging_branch
    dsc = args.dsc_file
    repo = args.repo_path
    git_repo = args.git_repo
    derived_branch = args.derived_branch
    if git_repo:
        try:
            repo = git_repo.split("/")[-1].split(".")[0]
            # repo = os.path.join()
        except:
            raise Exception(f"{git_repo} is not a valid git url")
        p.check_call(f"git clone {git_repo} {repo}", shell=True)
    assert repo and os.path.exists(repo), f"no such directory for {repo}"
    assert os.path.exists(os.path.join(repo, ".git")), f"{repo} is not a valid git repo"
    assert packaging_branch, f"please specify debian branch"

    p.check_call(
        'git branch -r | \
        grep -v "\->" | \
        while read remote; \
            do git branch --track "${remote#origin/}" "$remote"; \
        done || \
        true',
        shell=True,
        cwd=repo,
    )

    repo_branches = (
        p.check_output(f'git checkout upstream && git branch -a |grep -v -E "^remotes"', shell=True, cwd=repo)
        .decode("utf-8")
        .splitlines()
    )

    if packaging_branch in [b.strip() for b in repo_branches if b]:
        result = confirm(
            f"## {packaging_branch} branch already exists, whether to rebuild? [Y/N]: "
        )
        if result in ["n", "no"]:
            return print("## exit. no change.")
        else:
            p.check_call(
                f"git checkout upstream && git branch -D {packaging_branch} -D packaging/{packaging_branch} || true",
                shell=True,
                cwd=repo,
            )

    if derived_branch:
        p.check_call(f"git checkout {derived_branch}", shell=True, cwd=repo)
        p.check_call(f"git checkout -b {packaging_branch}", shell=True, cwd=repo)
        p.check_call(f"git checkout packaging/{derived_branch}", shell=True, cwd=repo)
        p.check_call(
            f"git checkout -b packaging/{packaging_branch}", shell=True, cwd=repo
        )
    else:
        setattr(args, "new_revisions", "")
        setattr(args, "suite", "")
        setattr(args, "save_old_changes", "")
        pkg = Package(dsc_file=dsc, source_path="")
        # rebuild source
        new_dsc = pkg.rebuild_source(args=args)

        p.check_call(
            f"gbp import-dsc --pristine-tar \
            --debian-branch={packaging_branch} \
            --create-missing-branches \
            --upstream-branch=upstream {new_dsc}",
            shell=True,
            cwd=repo,
        )
        p.check_call(
            "git restore --staged . && \
            git clean -f -d",
            shell=True,
            cwd=repo,
        )
        p.check_call(
            f"git checkout {packaging_branch} && \
            git branch packaging/{packaging_branch}",
            shell=True,
            cwd=repo,
        )
        p.check_call(
            "gbp pq import && gbp pq switch",
            shell=True,
            cwd=repo,
        )
        p.check_call(
            f"git merge patch-queue/{packaging_branch} && \
            git branch -D patch-queue/{packaging_branch}",
            shell=True,
            cwd=repo,
        )
        p.check_call(
            "rm -rf debian/patches && \
            echo '3.0 (native)' > debian/source/format && \
            git add . && \
            git commit -m 'change format'",
            shell=True,
            cwd=repo,
        )


if __name__ == "__main__":
    arg = argparse.ArgumentParser()
    sub_parser = arg.add_subparsers()
    rebuild_source_parser = sub_parser.add_parser(
        "rebuild-source", help="rebuild source from debian source"
    )
    rebuild_source_parser.add_argument(
        "-d",
        "--dsc-file", dest="dsc_file", help="the dsc file full path"
    )
    rebuild_source_parser.add_argument(
        "-p",
        "--source-path", dest="source_path", help="the debian source path with exacted"
    )
    rebuild_source_parser.add_argument(
        "-s",
        "--suite", dest="suite", help="distribution suite"
    )
    rebuild_source_parser.add_argument(
        "--save-old-changes",
        dest="save_old_changes",
        action="store_true",
        default=False,
        help="save old changes",
    )
    rebuild_source_parser.add_argument(
        "-n",
        "--new-revisions",
        dest="new_revisions",
        help="the revisions would replace the old revisions",
    )
    rebuild_source_parser.set_defaults(func=rebuild_source)
    import_to_git_parser = sub_parser.add_parser(
        "import-to-git", help="import debian source to git repo"
    )
    import_to_git_parser.add_argument(
        "-b",
        "--packaging-branch",
        dest="packaging_branch",
        default="openkylin/yangtze",
        help="the git branch for openkylin packaging",
    )
    import_to_git_parser.add_argument(
        "-d",
        "--dsc-file", required=True, dest="dsc_file", help="the dsc file"
    )
    import_to_git_parser.set_defaults(func=import_to_git)
    import_branch_parser = sub_parser.add_parser(
        "import-branch",
        help="import debian source as a new debian branch for exists git repo",
    )
    import_branch_parser.add_argument(
        "-b",
        "--packaging-branch",
        dest="packaging_branch",
        required=True,
        help="the git branch for openkylin packaging",
    )
    import_branch_parser.add_argument(
        "-p",
        "--repo-path",
        dest="repo_path",
        help="the exists git repo path for import to new branch",
    )
    import_branch_parser.add_argument(
        "-u",
        "--git-url",
        dest="git_repo",
        help="the exists git repo url to clone for import to new branch",
    )
    import_branch_parser.add_argument(
        "--derived-branch", dest="derived_branch", help="derived branch"
    )
    import_branch_parser.add_argument(
        "-d",
        "--dsc-file", dest="dsc_file", help="the dsc file"
    )
    import_branch_parser.set_defaults(func=import_branch)
    args = arg.parse_args()
    args.func(args)
